Blotch Library
Blotch
Library - our purpose-built toolkit designed to streamline Widget development.
It has many useful React
components
and hooks to help you create great Widgets quickly and easily. With a design system built in, specifically for E-ink displays, you can be sure your Widget will look great on any Blotch Smart Frame.
Why Use the Blotch Library?
The Blotch Library is designed to help you build beautiful, functional Widgets faster and with less code. Here are the key benefits:
E-ink Optimized
All components are specifically designed and optimized for E-ink displays, ensuring crisp visuals and excellent readability on your Blotch Smart Frame.
Developer Experience
Built with TypeScript and React, the library provides full type safety, autocomplete support, and a familiar development experience for modern web developers.
Ready-to-Use Components
Pre-built components for common Widget needs - charts, gauges, QR codes, and more - so you can focus on your Widget's unique features rather than reinventing the wheel.
Third-Party Integrations
Built-in OAuth support and integration helpers make it easy to connect your Widget to external services and APIs.
State Management
Simple, powerful hooks for managing state, storage, and data persistence across Frame updates.
Getting Started
For comprehensive, interactive documentation with live examples and code snippets, visit the Blotch Library Storybook.
- Live component previews - See components in action
- Interactive controls - Modify props and see real-time changes
- Code examples - Copy-paste ready code snippets
- Visual documentation - Playground and usage guidelines
- TypeScript definitions - Full type information for all components and hooks
Importing Components and Hooks
To use any component or hook from the Blotch Library in the Widget Designer, import them using the ~lib/
prefix:
import { ComponentName } from "~lib/package-name";
import { useHookName } from "~lib/hook-name";
The ~lib/
prefix is a special import path that works seamlessly in the Widget Designer environment.
Example Widget
Here's a simple example of a Widget using the Blotch Library:
import { useFetch } from "~lib/use-fetch";
import { BarIndicator } from "~lib/bar-indicator";
export const Widget = () => {
const { data, error, isLoading } = useFetch("https://api.example.com/stats");
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Stats Dashboard</h1>
<BarIndicator numerator={data.current} denominator={data.total} />
</div>
);
};
Available Components
Charts & Visualizations
BarIndicator
A horizontal progress bar perfect for showing completion percentages, progress, or ratios.
Import:
import { BarIndicator } from "~lib/bar-indicator";
Props:
Prop | Type | Required | Description |
---|---|---|---|
numerator | number | Yes | Current value |
denominator | number | Yes | Maximum value |
width | ${number}px | ${number}% | No | Width of the bar (default: "100%") |
Example:
<BarIndicator numerator={75} denominator={100} width="200px" />
DonutGauge
A circular gauge that displays a ratio in an elegant donut chart format.
Import:
import { DonutGauge } from "~lib/donut-gauge";
Props:
Prop | Type | Required | Description |
---|---|---|---|
numerator | number | Yes | Current value |
denominator | number | Yes | Maximum value |
Example:
<DonutGauge numerator={7} denominator={10} />
The DonutGauge automatically adapts to light and dark modes for optimal E-ink display.
LineChart
A versatile line chart component for displaying trends and time-series data.
Import:
import { LineChart } from "~lib/line-chart";
Props:
Prop | Type | Required | Description |
---|---|---|---|
points | number[] | [number, number][] | readonly [number, number][] | Yes | Data points to plot |
axis | { y?: { name?: string; visible?: boolean }; x?: { name?: string; visible?: boolean } } | No | Axis configuration |
Example:
// Simple array of Y values
<LineChart points={[10, 20, 15, 30, 25]} />
// With X,Y coordinates
<LineChart
points={[[0, 10], [1, 20], [2, 15], [3, 30], [4, 25]]}
axis={{
x: { visible: true },
y: { visible: true }
}}
/>
Scannable Components
QRCode
Generate QR codes for URLs, text, or any scannable data.
Import:
import { QRCode } from "~lib/qr-code";
Props:
Prop | Type | Required | Description |
---|---|---|---|
text | string | Yes | Content to encode in the QR code |
size | number | No | Size in pixels (default: 256) |
Example:
<QRCode text="https://blotch.app" size={200} />
Often useful if you want to link to a website or app from your Widget. For example if you want the user to read more information, or to get more context on their phone.
Available Hooks
Data Fetching
useFetch
Use useFetch
or useFetchProxy
for data fetching instead of native fetch to benefit from built-in error handling, loading states and caching.
A powerful hook for making HTTP requests with support for all HTTP methods, retries, and error handling.
Import:
import { useFetch } from "~lib/use-fetch";
Usage:
const { data, error, isLoading } = useFetch<ResponseType>(url, config);
Parameters:
Parameter | Type | Required | Description |
---|---|---|---|
url | string | Yes | The URL to fetch data from |
config | RequestInit | No | Fetch configuration options |
Return Value:
Property | Type | Description |
---|---|---|
data | T | The fetched data |
error | Error | Error object if request failed |
isLoading | boolean | Loading state |
Examples:
// GET request
const { data, error, isLoading } = useFetch("https://api.example.com/data");
// POST request with body
const { data, error, isLoading } = useFetch("https://api.example.com/data", {
method: "POST",
body: JSON.stringify({ key: "value" }),
headers: { "Content-Type": "application/json" },
});
// With retry logic
const { data, error, isLoading } = useFetch("https://api.example.com/data", {
retry: {
condition: (data) => data.status === "retry",
count: 5,
},
});
The fetch is executed when the component mounts and whenever the URL changes. If the URL is undefined or the component unmounts before data is retrieved, the fetch will not be executed.
useFetchProxy
A CORS-bypassing version of useFetch
that uses a proxy to fetch data.
Import:
import { useFetchProxy } from "~lib/use-fetch-proxy";
Usage:
const { data, error, isLoading } = useFetchProxy<ResponseType>(url, config);
Use useFetchProxy
when you need to fetch data from APIs that don't allow cross-origin requests. It's a drop-in replacement for useFetch
.
AI Integration
useAI
A simple hook for interacting with AI chat services.
Import:
import { useAI } from "~lib/use-ai";
Usage:
const { data, error } = useAI({ prompt: "Your prompt here" });
Parameters:
Parameter | Type | Required | Description |
---|---|---|---|
prompt | string | Yes | The text prompt to send to the AI |
Return Value:
Property | Type | Description |
---|---|---|
data | AIResponse | AI response with output |
error | Error | Error if request fails |
Example:
const ChatWidget = () => {
const { data, error } = useAI({
prompt: "Write a haiku about coding",
});
if (error) return <div>Error: {error.message}</div>;
if (!data) return <div>Loading...</div>;
return <div>{data.output}</div>;
};
Location Services
useGeocode
Convert addresses into geographic coordinates (latitude and longitude).
Import:
import { useGeocode } from "~lib/geocoding";
Usage:
const geocodeData = useGeocode("New York, NY, US");
Return Value:
Property | Type | Description |
---|---|---|
zip | string | Postal code |
name | string | Location name |
lat | number | Latitude coordinate |
lon | number | Longitude coordinate |
country | string | Country code |
Example:
const LocationWidget = () => {
const geocodeData = useGeocode("San Francisco, CA, US");
if (!geocodeData) return <div>Loading...</div>;
return (
<div>
<h2>{geocodeData.name}</h2>
<p>
Coordinates: {geocodeData.lat}, {geocodeData.lon}
</p>
</div>
);
};
For best results, use these formats:
- City, State, Country:
"New York, NY, US"
- City, Country:
"London, UK"
- Postal Code:
"10001, US"
State & Storage
useStorage
Persist data across Frame updates.
Import:
import { useStorage } from "~lib/use-storage";
Usage:
const [data, setData] = useStorage<T>();
Example:
const Widget = () => {
const [data, setData] = useStorage<{ count: number }>();
const increment = () => {
setData({ count: (data?.count || 0) + 1 });
};
return (
<div>
<p>Count: {data?.count || 0}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
Data stored with useStorage
persists between Widget updates, making it perfect for maintaining state like OAuth tokens, user preferences, or cached data.
UI State
useDarkMode
Detect the user's dark mode preference.
Import:
import { useDarkMode } from "~lib/use-dark-mode";
Usage:
const isDarkMode = useDarkMode();
Example:
const Widget = () => {
const isDarkMode = useDarkMode();
return (
<div
style={{
background: isDarkMode ? "#000" : "#fff",
color: isDarkMode ? "#fff" : "#000",
}}
>
<h1>{isDarkMode ? "🌙 Dark Mode" : "☀️ Light Mode"}</h1>
</div>
);
};
Third-Party Integrations
RequireIntegration
A component that conditionally renders content based on OAuth integration status.
Import:
import { RequireIntegration } from "~lib/integrations";
Props:
Prop | Type | Required | Description |
---|---|---|---|
integration | string | Yes | Integration identifier |
children | ReactNode | Yes | Content to render when authenticated |
Example:
<RequireIntegration integration="auth.my-domain.com">
<div>You are authenticated!</div>
</RequireIntegration>
useIntegration
Access OAuth integration data within a RequireIntegration
context.
Import:
import { useIntegration } from "~lib/integrations";
Usage:
const integration = useIntegration();
Return Value:
Property | Type | Description |
---|---|---|
host | string | The hostname of the integration |
accessToken | string | undefined | OAuth access token |
refreshToken | string | undefined | OAuth refresh token |
expiresIn | number | undefined | Token expiration time (seconds) |
storedAt | number | Timestamp when token was stored |
widgetUid | string | Unique identifier for the widget |
redirectUri | string | Redirect URI for authentication |
Example:
<RequireIntegration integration="api.spotify.com">
{() => {
const integration = useIntegration();
const { data } = useFetch("https://api.spotify.com/v1/me", {
headers: {
Authorization: `Bearer ${integration.accessToken}`,
},
});
return <div>Welcome, {data?.display_name}!</div>;
}}
</RequireIntegration>
useIntegration
must be used within a component wrapped by RequireIntegration
, otherwise it will return null
.